iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0
Modern Web

擁抱 .Net Core系列 第 6

[Day6] 泛型主機(Host) - 1

  • 分享至 

  • xImage
  •  

前言

Host,官方機翻叫做主機,是一個封裝了應用程式資源以及生命週期的物件,包含了

  • Dependency injection (相依性注入,DI)
  • Logging (紀錄)
  • Configuration (設定)
  • App shutdown (應用程式關機)
  • IHostedService implementations (承載服務的實作)

管理應用程式生命週期的 Host的物件,主要封裝在nuget套件 Microsoft.Extensions.Hosting

IHostService

當Host 啟動的時候,他會呼叫每個被註冊在容器中的IHostServiceStartAsync() 方法
通常一個IHostService 代表他一個需要被長時間執行的Service
像是AspNet Core 的Web應用程式,本質上就是一個不斷監聽著http 請求,並對齊請求做出回應的一個HostService

IHostService.cs

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

我這邊寫了一個範例程式,每五秒會去檢查當前庫存,如果庫存量低於30會跳alert
NotificationService.cs

public class NotificationService : IHostedService
{
    private readonly Random _random;

    public NotificationService(Random random)
    {
        _random = random;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        const int alertStock = 30;
        while (cancellationToken.IsCancellationRequested == false)
        {
            var stock = GetRemainStock();
            Console.WriteLine($"Current Stock: {stock}");
            
            if (stock <= alertStock)
            {
                AlertLowStock(stock);
            }
            
            await Task.Delay(5000, cancellationToken);
        }
    }

    private static void AlertLowStock(int stock)
    {
        Console.WriteLine($"Stock is low. Current stock is {stock}");
    }

    private int GetRemainStock()
    {
        return _random.Next(0,101);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Stock checking monitor stopped");
        return Task.CompletedTask;
    }
}

這邊使用HostBuilder 物件來建立一個IHost
我們前面有提到 Host 封裝了 DI
可以看見下面是使用 HostBuilder 來註冊服務
後面會提到其他HostBuilder的使用方式
Program.cs

IHostBuilder builder = new HostBuilder();

IHost host = builder.ConfigureServices(service =>
{
    service.AddSingleton(new Random());
    service.AddHostedService<StockMonitorService>();
}).Build();

await host.RunAsync();

https://ithelp.ithome.com.tw/upload/images/20220917/20109549816ITSD9i6.png

生命週期

可以注意到我們上面在啟動Host的時候呼叫的是 await host.RunAsync(); 而不是 await host.StartAsync()
我們來看看為什麼

IHostApplicationLifetime

IHostApplicationLifetime.cs

public interface IHostApplicationLifetime
{
    /// <summary>
    /// Triggered when the application host has fully started.
    /// </summary>
    CancellationToken ApplicationStarted { get; }

    /// <summary>
    /// Triggered when the application host is starting a graceful shutdown.
    /// Shutdown will block until all callbacks registered on this token have completed.
    /// </summary>
    CancellationToken ApplicationStopping { get; }

    /// <summary>
    /// Triggered when the application host has completed a graceful shutdown.
    /// The application will not exit until all callbacks registered on this token have completed.
    /// </summary>
    CancellationToken ApplicationStopped { get; }

    /// <summary>
    /// Requests termination of the current application.
    /// </summary>
    void StopApplication();
}

這個介面主要有3個CancellationToken 物件
分別對應

  1. Host啟動完成時
  2. Host開始關閉時
  3. Host關閉完成時

我們可以透過對他們來取得對應的通知

Program.cs

IHostBuilder builder = new HostBuilder();

var host = builder.ConfigureServices(service =>
{
    service.AddSingleton(new Random());
    service.AddHostedService<LifetimeSampleService>();
}).Build();

await host.RunAsync();

public class LifetimeSampleService : IHostedService
{
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public LifetimeSampleService(IHostApplicationLifetime hostApplicationLifetime)
    {
        hostApplicationLifetime.ApplicationStarted.Register(()=> Console.WriteLine("Application started"));
        hostApplicationLifetime.ApplicationStopping.Register(()=> Console.WriteLine("Application stopping"));
        hostApplicationLifetime.ApplicationStopped.Register(()=> Console.WriteLine("Application stopped"));
        _hostApplicationLifetime = hostApplicationLifetime;
    }
    
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var token = new CancellationTokenSource(TimeSpan.FromSeconds(3)).Token;
        token.Register(_hostApplicationLifetime.StopApplication);
        
        await Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await Task.CompletedTask;
    }
}

這邊透過CancellationToken 物件的Register 方法註冊了callback
然後在Start的時候 設定一個CancellationTokenSource 會在三秒後去觸發 StopApplication 的方法

https://ithelp.ithome.com.tw/upload/images/20220917/20109549uoqbj0TMRT.png

RunAsync

這個方法本身是個擴充方法

(這邊簡化不全貼整個Extentsion,只貼RunAsync())

public static class HostingAbstractionsHostExtensions
{
    public static async Task RunAsync(this IHost host, CancellationToken token = default)
    {
        try
        {
            await host.StartAsync(token).ConfigureAwait(false);

            await host.WaitForShutdownAsync(token).ConfigureAwait(false);
        }
        finally
        {
            if (host is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync().ConfigureAwait(false);
            }
            else
            {
                host.Dispose();
            }
        }
    }
}

很簡單就三個步驟

  1. 呼叫host.StartAsync 方法
  2. 呼叫host.WaitForShutdownAsync 方法
  3. Dispose()

... 等等這個WaitForShutdownAsync是什麼
這邊不貼程式碼,大概說一下他的實作方式為
監聽 IHostApplicationLifetimeApplicationStopping 是不是觸發了
是的話叫呼叫IHost.StopAsync()的方法


這邊以下打到一半不小心按到上一頁/images/emoticon/emoticon02.gif,因為太氣了,可能直接省略了一些內容

IHostBuilder

在.Net core的設定中,很多地方都可以看到Build Pattern,她將複雜物件的建立抽象化
IHostBuilder 就是其中一個例子

我們前面有提到Host一個封裝了應用程式資源的物件,所以裡面所要用的服務都可以透過IHostBuilder 設定

IHostBuilder.cs

 public interface IHostBuilder
{

    IDictionary<object, object> Properties { get; }

    IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate);

    IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);

    IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate);

    IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull;

    IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull;

    IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);

    IHost Build();
}

ConfigureHostConfigurationConfigureAppConfiguration 這兩個方法主要是Host跟應用程式的設定,後面提到 Configuration 會再用到。

builder.ConfigureHostConfiguration(x => x.AddJsonFile("myconfig.json"))

剩下幾個方法主要都是對DI 進行設定
我們昨天有提到,asp.net core 的DI 容器主要是透過 DefaultServiceProviderFactory 所建立的
就算你不是dotnet core應用程式也可以自訂你的DI Factory

builder.UseServiceProviderFactory(x => new DefaultServiceProviderFactory())

其他像log 之類的Config方式則放在另一個Extension class HostingHostBuilderExtensions.cs 原始碼就不特別貼了,在後面講log 的時候會再提到

HostBuilderContext

可以注意到在IHostBuilder 中的 config 方法很多都有類別HostBuilderContext 的存在

HostBuilderContext.cs

public class HostBuilderContext
{
    public HostBuilderContext(IDictionary<object, object> properties)
    {
        ThrowHelper.ThrowIfNull(properties);

        Properties = properties;
    }

    public IHostEnvironment HostingEnvironment { get; set; } = null!;
    public IConfiguration Configuration { get; set; } = null!;
    public IDictionary<object, object> Properties { get; }
}

ConfigureHostConfigurationConfigureAppConfiguration 這兩個方法中產生Configuration 最終彙整成同一份IConfiguration

Properties 則是在做設定時,有前後相依同個變數,可以從其中去取得

IHostEnvironment

我們常常會需要根據不同的執行環境(ex. 本機,生產)做不同的事情
這些資訊一樣會封裝在HostBuilderContext.HostingEnvironment
可以透過擴充方法IHostEnvironment.IsEnvironment(string environment) 來判斷當前環境是否為你想要的環境

IHostEnvironment.cs

public interface IHostEnvironment
{
    string EnvironmentName { get; set; }

    string ApplicationName { get; set; }

    string ContentRootPath { get; set; }

    IFileProvider ContentRootFileProvider { get; set; }
}

這邊做個Sample

IHostBuilder builder = new HostBuilder();

// HostBuilder 如果沒特別設定,預設環境為 Production
args = args.Append("environment=Development").ToArray();

var host =
    // 設定Config(包含環境)
    .ConfigureHostConfiguration(x => x.AddCommandLine(args))
    .ConfigureServices(x => x.AddHostedService<EnvSampleService>())
    .Build();

await host.RunAsync();

public class EnvSampleService : IHostedService
{
    private readonly IHostEnvironment _env;

    public EnvSampleService(IHostEnvironment env)
    {
        _env = env;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine($"Environment: {_env.EnvironmentName}");
        Console.WriteLine($"isDevelopment: {_env.IsEnvironment("Development")}");
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await Task.CompletedTask;
    }
}

我們透過arg 設定當前環境為Development
https://ithelp.ithome.com.tw/upload/images/20220917/20109549LPJ80LNYWj.png

Build Host

這邊節錄了HostBuilder 的Build 方法

HostBuilder.cs

public IHost Build()
{
    if (_hostBuilt)
    {
        throw new InvalidOperationException(SR.BuildCalled);
    }
    _hostBuilt = true;

    // REVIEW: If we want to raise more events outside of these calls then we will need to
    // stash this in a field.
    using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
    const string hostBuildingEventName = "HostBuilding";
    const string hostBuiltEventName = "HostBuilt";

    if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
    {
        Write(diagnosticListener, hostBuildingEventName, this);
    }


    // 我們把重點放在這段以下
    BuildHostConfiguration();
    CreateHostingEnvironment();
    CreateHostBuilderContext();
    BuildAppConfiguration();
    CreateServiceProvider();

    // 可以看見Host 物件是由DI容器提供,並非由Build方法建立
    var host = _appServices.GetRequiredService<IHost>();
    if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
    {
        Write(diagnosticListener, hostBuiltEventName, host);
    }

    return host;
}

建立Host 大概有幾個步驟

  1. 建立 HostConfiguration (IConfiguration)
  2. 建立IHostEnvironment
  3. 建立HostBuilderContext 其實就是把剛剛那兩個塞進去
  4. 建立 AppConfiguration,會跟 HostConfiguration 整理成一份IConfiguration 再assign回HostBuilderContext.IConfiguration
  5. 建立DI Container ,會在這步建立Host (CreateServiceProvider())
  6. 透過DI Container 取Host

這邊針對2跟5做補充

CreateHostingEnvironment
我在上面的例子有提到,CreateBuilder 如無特別設,當前環境預設會是Production,原因就在這個方法中
https://ithelp.ithome.com.tw/upload/images/20220917/201095499yJTz6EWpv.png

private void CreateServiceProvider()
{
    var services = new ServiceCollection();
#pragma warning disable CS0618 // Type or member is obsolete
    services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete

    // 這邊註冊了IHostEnvironment,所以可以在服務中注入
    services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
    services.AddSingleton(_hostBuilderContext);
    // register configuration as factory to make it dispose with the service provider
    services.AddSingleton(_ => _appConfiguration);
#pragma warning disable CS0618 // Type or member is obsolete
    services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
    services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();

    AddLifetime(services);

    // Host 在這裡建立
    services.AddSingleton<IHost>(_ =>
    {
        return new Internal.Host(_appServices,
            _hostingEnvironment,
            _defaultProvider,
            _appServices.GetRequiredService<IHostApplicationLifetime>(),
            _appServices.GetRequiredService<ILogger<Internal.Host>>(),
            _appServices.GetRequiredService<IHostLifetime>(),
            _appServices.GetRequiredService<IOptions<HostOptions>>());
    });
    services.AddOptions().Configure<HostOptions>(options => { options.Initialize(_hostConfiguration); });
    services.AddLogging();

    // 這邊會把我們在外面註冊的Service加到IServiceCollection 中
    foreach (Action<HostBuilderContext, IServiceCollection> configureServicesAction in _configureServicesActions)
    {
        configureServicesAction(_hostBuilderContext, services);
    }

    object containerBuilder = _serviceProviderFactory.CreateBuilder(services);

    foreach (IConfigureContainerAdapter containerAction in _configureContainerActions)
    {
        containerAction.ConfigureContainer(_hostBuilderContext, containerBuilder);
    }

    _appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);

    if (_appServices == null)
    {
        throw new InvalidOperationException(SR.NullIServiceProvider);
    }

    // resolve configuration explicitly once to mark it as resolved within the
    // service provider, ensuring it will be properly disposed with the provider
    _ = _appServices.GetService<IConfiguration>();
}

上一篇
[Day5] .Net Core 中的相依性注入 - 2
下一篇
[Day7] asp.net core 中的Web主機(Host) - 2
系列文
擁抱 .Net Core30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言